﻿	using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;
#if WinRT
    using Windows.UI.Xaml;
    using Windows.UI.Xaml.Controls;
    using Windows.UI.Xaml.Controls.Primitives;
    using Windows.UI.Xaml.Data;
    using Windows.UI.Xaml.Markup;
    using TriggerBase = Windows.UI.Interactivity.TriggerBase;
    using EventTrigger = Windows.UI.Interactivity.EventTrigger;
    using Windows.UI.Xaml.Shapes;
#else
using System.ComponentModel;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Controls.Primitives;
    using System.Windows.Data;
    using System.Windows.Markup;
    using System.Windows.Shapes;
	using TriggerBase = System.Windows.Interactivity.TriggerBase;
    using EventTrigger = System.Windows.Interactivity.EventTrigger;
#endif
#if !SILVERLIGHT && !WinRT
    using System.Windows.Documents;
#endif

namespace DailyFx.UI
{
	
    /// <summary>
    ///		Represents the conventions for a particular type of element .
    /// </summary>
    public class Convention
    {
        /// <summary>
        ///		The type of element to which the conventions apply.
        /// </summary>
        public Type ElementType;

        /// <summary>
        /// Gets the default property to be used in binding conventions.
        /// </summary>
        public Func<DependencyObject, DependencyProperty> GetBindableProperty;

        /// <summary>
        /// The default trigger to be used when wiring actions on this element.
        /// </summary>
        public Func<TriggerBase> CreateTrigger;

        /// <summary>
        /// The default property to be used for parameters of this type in actions.
        /// </summary>
        public string ParameterProperty;

        /// <summary>
        /// Applies custom conventions for elements of this type.
        /// </summary>
        /// <remarks>Pass the view model type, property path, property instance, framework element and its convention.</remarks>
        public Func<Type, string, PropertyInfo, FrameworkElement, Convention, bool> 
			ApplyBinding = ( modelType, path, property, element, convention ) => 
			{ 
				return ConventionManager.ApplyBinding ( 
					modelType, path, property, element, convention, convention.GetBindableProperty(element) );
			};
    }



    /// <summary>
    ///		Used to configure the conventions used by the framework to apply bindings and create actions.
    /// </summary>
    public static class ConventionManager 
	{
        static readonly ILog Log = LogManager.GetLog( typeof(ConventionManager) );

        /// <summary>
        ///		Converters <see cref="bool"/> to/from <see cref="Visibility"/>.
        /// </summary>
        public static IValueConverter BooleanToVisibilityConverter = new BooleanToVisibilityConverter();


        /// <summary>
        ///		The default DataTemplate used for ItemsControls when required.
        /// </summary>
        public static DataTemplate DefaultItemTemplate = (DataTemplate)
#if SILVERLIGHT || WinRT
        XamlReader.Load (
#else
        XamlReader.Parse (
#endif
#if WinRT
            "<DataTemplate xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' " +
						  "xmlns:cal='using:DailyFx.UI'>" +
                "<ContentControl ui:View.Model=\"{Binding}\" IsTabStop=\"False\" " +
								VerticalContentAlignment=\"Stretch\" " +
								HorizontalContentAlignment=\"Stretch\" />" +
            "</DataTemplate>"
#else
             "<DataTemplate xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' " +
                           "xmlns:ui='clr-namespace:DailyFx.UI;assembly=DailyFx.UI'> " +
                "<ContentControl ui:View.Model=\"{Binding}\" IsTabStop=\"False\" " + 
								"VerticalContentAlignment=\"Stretch\" " +  
								"HorizontalContentAlignment=\"Stretch\" />" +
            "</DataTemplate>"
#endif
		);

        /// <summary>
        ///		The default DataTemplate used for Headered controls when required.
        /// </summary>
        public static DataTemplate DefaultHeaderTemplate = (DataTemplate)
#if SILVERLIGHT || WinRT
        XamlReader.Load (
#else
        XamlReader.Parse (
#endif
            "<DataTemplate xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'>" +
				"<TextBlock Text=\"{Binding DisplayText, Mode=TwoWay}\" />" +
			"</DataTemplate>"
        );


		/// <summary>
		/// 
		/// </summary>
        static readonly Dictionary<Type, Convention> ElementConventions 
			= new Dictionary<Type, Convention>();


        /// <summary>
        ///		Creates a binding and sets it on the element, applying the appropriate conventions.
        /// </summary>
        public static Action<Type, String, PropertyInfo, FrameworkElement, Convention, DependencyProperty> 
			SetBinding = ( modelType, path, property, element, convention, bindableProperty ) => 
			{
#if WinRT
                var binding = new Binding { Path = new PropertyPath(path) };
#else
                var binding = new Binding( path );
#endif

                SetBindingMode	( binding, property );
                SetValueConverter	( binding, bindableProperty, property );
                SetStringFormat	( binding, convention, property );
                SetValidation		( binding, modelType, property );
                SetUpdateSourceTrigger( bindableProperty, element, binding, property );

				// set to element .
                BindingOperations.SetBinding ( element, bindableProperty, binding );
            };

        /// <summary>
        ///		Applies the appropriate binding mode to the binding.
        /// </summary>
        public static Action<Binding, PropertyInfo> SetBindingMode = (binding, property) =>
		{
#if WinRT
            var setMethod = property.SetMethod;
            binding.Mode = (property.CanWrite && setMethod != null && setMethod.IsPublic) ? BindingMode.TwoWay : BindingMode.OneWay;
#else
            var setMethod = property.GetSetMethod();
            binding.Mode = (property.CanWrite && setMethod != null && setMethod.IsPublic) ? BindingMode.TwoWay : BindingMode.OneWay;
#endif
        };

        /// <summary>
        ///		Determines whether or not and what type of validation to enable on the binding.
        /// </summary>
        public static Action<Binding, Type, PropertyInfo> SetValidation = ( binding, modelType, property) =>
		{
#if SILVERLIGHT || NET45
            if ( typeof(INotifyDataErrorInfo).IsAssignableFrom(modelType) )
			{
                binding.ValidatesOnNotifyDataErrors = true;
                binding.ValidatesOnExceptions = true;
            }
#endif
#if !WinRT
            if ( typeof(IDataErrorInfo).IsAssignableFrom(modelType) )
			{
                binding.ValidatesOnDataErrors = true;
                binding.ValidatesOnExceptions = true;
            }
#endif
        };

        /// <summary>
        ///		Determines whether a value converter is is needed and applies one to the binding.
        /// </summary>
        public static Action<Binding, DependencyProperty, PropertyInfo> SetValueConverter = (binding, bindableProperty, property) => 
		{
            if ( bindableProperty == UIElement.VisibilityProperty 
				&& typeof(bool).IsAssignableFrom(property.PropertyType) )
			{
				binding.Converter = BooleanToVisibilityConverter;
			}
        };

        /// <summary>
        ///		Determines whether a custom string format is needed and applies it to the binding.
        /// </summary>
        public static Action<Binding, Convention, PropertyInfo> SetStringFormat = (binding, convention, property) => 
		{
#if !WinRT
            if(typeof(DateTime).IsAssignableFrom(property.PropertyType))
                binding.StringFormat = "{0:d}";
#endif
        };

        /// <summary>
        /// Determines whether a custom update source trigger should be applied to the binding.
        /// </summary>
        public static Action<DependencyProperty, DependencyObject, Binding, PropertyInfo> 
			SetUpdateSourceTrigger = (bindableProperty, element, binding, info) =>
		{
#if SILVERLIGHT && !SL5
			// Accounts for the lack of UpdateSourceTrigger in silverlight.
			// todo:
			
#elif !(WinRT && !WinRT81)
			binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
#endif
        };

        static ConventionManager()
		{
#if !WINDOWS_PHONE && !WinRT
			AddConvention<DatePicker>(DatePicker.SelectedDateProperty, "SelectedDate", "SelectedDateChanged");
#endif
#if WinRT81
            AddElementConvention<DatePicker>(DatePicker.DateProperty, "Date", "DateChanged");
            AddElementConvention<TimePicker>(TimePicker.TimeProperty, "Time", "TimeChanged");
            AddElementConvention<SearchBox>(SearchBox.QueryTextProperty, "QueryText", "QuerySubmitted");
            AddElementConvention<Hub>(Hub.HeaderProperty, "Header", "Loaded");
            AddElementConvention<HubSection>(HubSection.HeaderProperty, "Header", "SectionsInViewChanged");
#endif
#if WinRT
            AddElementConvention<ToggleSwitch>(ToggleSwitch.IsOnProperty, "IsOn", "Toggled");
            AddElementConvention<ProgressRing>(ProgressRing.IsActiveProperty, "IsActive", "Loaded");
            AddElementConvention<Slider>(Slider.ValueProperty, "Value", "ValueChanged");
            AddElementConvention<RichEditBox>(RichEditBox.DataContextProperty, "DataContext", "TextChanged");
#endif
#if SILVERLIGHT || WinRT
            AddElementConvention<HyperlinkButton>(HyperlinkButton.ContentProperty, "DataContext", "Click");
            AddElementConvention<PasswordBox>(PasswordBox.PasswordProperty, "Password", "PasswordChanged");
#else
			AddConvention<PasswordBox>(null, "Password", "PasswordChanged");
			AddConvention<Hyperlink>(Hyperlink.DataContextProperty, "DataContext", "Click");
			AddConvention<RichTextBox>(RichTextBox.DataContextProperty, "DataContext", "TextChanged");
			AddConvention<System.Windows.Controls.Menu>(System.Windows.Controls.Menu.ItemsSourceProperty, "DataContext", "Click");
			AddConvention<MenuItem>(MenuItem.ItemsSourceProperty, "DataContext", "Click");
			AddConvention<Label>(Label.ContentProperty, "Content", "DataContextChanged");
			AddConvention<Slider>(Slider.ValueProperty, "Value", "ValueChanged");
			AddConvention<Expander>(Expander.IsExpandedProperty, "IsExpanded", "Expanded");
			AddConvention<StatusBar>(StatusBar.ItemsSourceProperty, "DataContext", "Loaded");
			AddConvention<ToolBar>(ToolBar.ItemsSourceProperty, "DataContext", "Loaded");
			AddConvention<ToolBarTray>(ToolBarTray.VisibilityProperty, "DataContext", "Loaded");
			AddConvention<TreeView>(TreeView.ItemsSourceProperty, "SelectedItem", "SelectedItemChanged");
			AddConvention<TabControl>(TabControl.ItemsSourceProperty, "ItemsSource", "SelectionChanged");

			AddConvention<TabItem>(TabItem.ContentProperty, "DataContext", "DataContextChanged");
			AddConvention<System.Windows.Window>(System.Windows.Window.DataContextProperty, "DataContext", "Loaded");
#endif
			AddConvention<UserControl>(UserControl.VisibilityProperty, "DataContext", "Loaded");
			AddConvention<Image>(Image.SourceProperty, "Source", "Loaded");
			AddConvention<ToggleButton>(ToggleButton.IsCheckedProperty, "IsChecked", "Click");
			AddConvention<ButtonBase>(ButtonBase.ContentProperty, "DataContext", "Click");
			AddConvention<TextBox>(TextBox.TextProperty, "Text", "TextChanged");
			AddConvention<TextBlock>(TextBlock.TextProperty, "Text", "DataContextChanged");
			AddConvention<ProgressBar>(ProgressBar.ValueProperty, "Value", "ValueChanged");
			AddConvention<Selector>(Selector.ItemsSourceProperty, "SelectedItem", "SelectionChanged");
			// => 
			// { todo:
			// }
			AddConvention<ItemsControl>(ItemsControl.ItemsSourceProperty, "DataContext", "Loaded")
				.ApplyBinding = (viewModelType, path, property, element, convention) =>
				{
					if (!ApplyBinding2(viewModelType, path, property, element, convention, ItemsControl.ItemsSourceProperty))
					{
						return false;
					}

					ApplyItemTemplate((ItemsControl)element, property);

					return true;
				};
			AddConvention<ContentControl>(ContentControl.ContentProperty, "DataContext", "Loaded")
				.GetBindableProperty = delegate(DependencyObject foundControl)
				{
					var element = (ContentControl)foundControl;

					if (element.Content is DependencyObject)
						return null;
#if SILVERLIGHT
                    var useViewModel = element.ContentTemplate == null;
#else
					var useViewModel = element.ContentTemplate == null && element.ContentTemplateSelector == null;
#endif
					if (useViewModel)
					{
						Log.Info("ViewModel bound on {0}.", element.Name);
						return View.ModelProperty;
					}

					Log.Info("Content bound on {0}. Template or content was present.", element.Name);
					return ContentControl.ContentProperty;
				};

			AddConvention<Shape>(Shape.VisibilityProperty, "DataContext", "MouseLeftButtonUp");
			AddConvention<FrameworkElement>(FrameworkElement.VisibilityProperty, "DataContext", "Loaded");
		}

        /// <summary>
        ///		Adds an element convention.
        /// </summary>
        public static Convention AddConvention<T> ( DependencyProperty bindableProperty, string parameterProperty, string eventName )
		{
			var convention = new Convention 
			{	
				ElementType = typeof(T) ,
                GetBindableProperty = element => bindableProperty,
                ParameterProperty = parameterProperty,
                CreateTrigger = () => new EventTrigger { EventName = eventName }
            };

			return ConventionManager.ElementConventions[ typeof(T)] = convention;
        }


        /// <summary>
        ///		Gets an element convention for the provided element type.
        /// </summary>
        public static Convention GetConvention ( Type elementType ) 
		{
            if ( elementType == null )
                return null;

            Convention propertyConvention;
            ElementConventions.TryGetValue( elementType, out propertyConvention );
#if WinRT
            return propertyConvention ?? GetElementConvention(elementType.GetTypeInfo().BaseType);
#else
            return propertyConvention ?? GetConvention(elementType.BaseType);
#endif
        }

        /// <summary>
        ///		Determines whether a particular dependency property already has a binding on the provided element.
        /// </summary>
        public static bool HasBinding ( FrameworkElement element, DependencyProperty property )
		{
#if NET
            return BindingOperations.GetBindingBase(element, property) != null;
#elif WinRT && !WinRT81
            var localValue = element.ReadLocalValue(property);

            if (localValue == DependencyProperty.UnsetValue)
                return false;

            return localValue.GetType().FullName == "System.__ComObject";
#else
            return element.GetBindingExpression(property) != null;
#endif
        }

        /// <summary>
        ///		Creates a binding and sets it on the element, guarding against pre-existing bindings.
        /// </summary>
        public static bool ApplyBinding ( Type modelType, string path, PropertyInfo property, 
			FrameworkElement element, Convention convention, DependencyProperty bindableProperty ) 
		{
            if ( bindableProperty == null || HasBinding(element, bindableProperty) ) 
			{
                return false;
            }

            SetBinding ( modelType, path, property, element, convention, bindableProperty );
            return true;
        }

        /// <summary>
        ///		Creates a binding and set it on the element, guarding against pre-existing bindings and pre-existing values.
        /// </summary>
        public static bool ApplyBinding2 (Type modelType, string path, PropertyInfo property, 
			FrameworkElement element, Convention convention, DependencyProperty bindableProperty) 
		{
            if ( bindableProperty == null || HasBinding(element, bindableProperty) ) 
			{
                return false;
            }
            if ( element.GetValue(bindableProperty) != null ) 
			{
                return false;
            }

            SetBinding( modelType, path, property, element, convention, bindableProperty );
            return true;
        }

        /// <summary>
        ///		Attempts to apply the default item template to the items control.
        /// </summary>
        public static void ApplyItemTemplate ( ItemsControl itemsControl, PropertyInfo property )
		{
            if ( !string.IsNullOrEmpty( itemsControl.DisplayMemberPath )
                || HasBinding(itemsControl, ItemsControl.DisplayMemberPathProperty)
                || itemsControl.ItemTemplate != null )
			{
                return;
            }

#if !WinRT
            if ( property.PropertyType.IsGenericType ) 
			{
                var itemType = property.PropertyType.GetGenericArguments().First();
                if (itemType.IsValueType || typeof(string).IsAssignableFrom(itemType))
				{
                    return;
                }
            }
#else
            if ( property.PropertyType.GetTypeInfo().IsGenericType )
			{
                var itemType = property.PropertyType.GenericTypeArguments.First();
                if ( itemType.GetTypeInfo().IsValueType || typeof (string).IsAssignableFrom(itemType) )
				{
                    return;
                }
            }
#endif

#if !SILVERLIGHT
            if (itemsControl.ItemTemplateSelector == null)
			{
                itemsControl.ItemTemplate = DefaultItemTemplate;
                Log.Info("ItemTemplate applied to {0}.", itemsControl.Name);
            }
#else
            itemsControl.ItemTemplate = DefaultItemTemplate;
            Log.Info("ItemTemplate applied to {0}.", itemsControl.Name);
#endif
        }

        
        /// <summary>
        ///		Applies a header template based on <see cref="IUIDisplayText"/>
        /// </summary>
        public static void ApplyHeaderTemplate ( FrameworkElement element, Type modelType, 
			DependencyProperty headerTemplateProperty, DependencyProperty headerTemplateSelectorProperty )
		{
            var template = element.GetValue(headerTemplateProperty);
            var selector = headerTemplateSelectorProperty != null
                               ? element.GetValue(headerTemplateSelectorProperty)
                               : null;

            if (template != null || selector != null || !typeof(IUIDisplayText).IsAssignableFrom(modelType))
			{
                return;
            }

            element.SetValue(headerTemplateProperty, DefaultHeaderTemplate);
            Log.Info("Header template applied to {0}.", element.Name);
        }

    }
}
